<template>
  <div>
    <!-- 最上方的表单 -->
    <el-form
      v-show="!previewMode"
      :inline="true"
      size="medium"
      @submit.native.prevent
      @keyup.enter.native="searchActive(searchName, currentDir)"
    >
      <span v-if="showSearch && searchMeta && searchFiles" class="search">
        <el-form-item :label="searchTitle">
          <el-autocomplete
            v-model="searchName"
            :placeholder="searchPlaceholder"
            :fetch-suggestions="querySearchAsync"
            :trigger-on-focus="false"
            value-key="name"
            size="mini"
          ></el-autocomplete>
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            @click="searchActive(searchName, currentDir)"
            size="small"
            >查询</el-button
          >
        </el-form-item>
      </span>
      <el-form-item v-if="showSeparator">|</el-form-item>
      <el-form-item v-if="showMkdir && makeDirectory">
        <el-button type="primary" @click="isMkdir = true" size="small"
          >新建文件夹</el-button
        >
      </el-form-item>
      <el-form-item v-if="showUpload">
        <el-button type="primary" @click="isUploading = true" size="small"
          >上传文件</el-button
        >
      </el-form-item>
    </el-form>
    <!-- 表格 -->
    <div v-show="!previewMode" style="height: 87%">
      <el-breadcrumb separator-class="el-icon-arrow-right" style="height: 2%">
        <el-breadcrumb-item v-for="(dir, index) in historyDir" :key="index"
          ><span class="breadcrumb" @click="jumpTo(dir)">{{
            dir.name
          }}</span></el-breadcrumb-item
        >
      </el-breadcrumb>
      <el-table
        v-if="(loading || tableData) && !isError"
        :data="tableData"
        :key="tableKey"
        stripe
        v-loading="loading"
        element-loading-text="拼命加载中"
        element-loading-background="rgba(245, 245, 245, 0.9)"
        @row-click="rowClicked"
        class="el-table-box"
      >
        <el-table-column prop="name" :label="labelTitle"> </el-table-column>
        <el-table-column v-if="showDescription" label="简介">
          <template slot-scope="scope">
            <span v-if="scope.row['description']">{{
              scope.row["description"]
            }}</span>
          </template>
        </el-table-column>
        <el-table-column v-if="showSize" label="文件大小" width="160">
          <template slot-scope="scope">
            <template v-if="scope.row.size">
              <span v-if="scope.row.size < 1024"
                >{{ Math.round(scope.row.size) }} Byte</span
              >
              <span v-else-if="scope.row.size / 1024 < 1024"
                >{{ (scope.row.size / 1024).toFixed(1) }} KB</span
              >
              <span v-else-if="scope.row.size / 1024 / 1024 < 1024"
                >{{ (scope.row.size / 1024 / 1024).toFixed(1) }} MB</span
              >
              <span v-else
                >{{ (scope.row.size / 1024 / 1024 / 1024).toFixed(1) }} GB</span
              >
            </template>
            <template v-else-if="scope.row.isDir">
              <span v-if="scope.row.length">{{ scope.row.length }}个文件</span>
            </template>
          </template>
        </el-table-column>
        <el-table-column
          v-if="showType"
          align="center"
          label="文件类型"
          width="160"
        >
          <template slot-scope="scope">
            <span v-if="scope.row['ext']">{{ scope.row["ext"] }}</span>
            <span v-else>文件夹</span>
          </template>
        </el-table-column>
        <el-table-column
          v-if="showModifyTime"
          label="修改时间"
          width="230"
          align="center"
        >
          <template v-if="scope.row['lastModifyTime']" slot-scope="scope">
            <span>{{ msFormat(scope.row["lastModifyTime"]) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          v-if="showOperator"
          label="操作"
          width="260"
          align="center"
        >
          <template slot-scope="scope">
            <template v-if="scope.row.isDir !== true">
              <span
                class="link"
                v-if="previewSource"
                @click="preview(scope.row)"
                >浏览</span
              >
              <span
                v-if="downloadFile"
                @click="downloadFile(scope.row)"
                class="link"
                >下载</span
              >
            </template>
            <template v-else>
              <span class="link" @click="openDir(scope.row)">打开文件夹</span>
            </template>
            <span
              v-if="changeFileMeta"
              class="link"
              @click="onFileChange(scope.row)"
              >修改</span
            >
            <span
              v-if="deleteFile"
              class="link"
              @click="onDelete(scope.row, currentDir, currentPage)"
              >删除</span
            >
          </template>
        </el-table-column>
      </el-table>
      <el-card
        class="error-card"
        v-else
        style="height: 97%; width: 100%"
        @click.native="retry"
      >
        <div style="padding-left: 25px">
          <h1 style="color: #f56c6c">
            <i
              class="el-icon-warning"
              style="font-size: 1.8em; margin-right: 0.5em"
            ></i
            >糟糕！
          </h1>
          <p style="font-size: 1.5em">
            {{ labelTitle }}列表加载出错了，点击重试。
          </p>
          <p>错误原因：{{ lastError }}</p>
        </div>
      </el-card>
    </div>
    <!-- 分页 -->
    <div style="float: right">
      <div v-show="!previewMode" class="pagination">
        <el-pagination
          @current-change="pageChange"
          :current-page.sync="currentPage"
          :page-sizes="[12, 15]"
          @size-change="sizeChange"
          :page-size="pageSize"
          layout="total, sizes,prev, pager, next,jumper"
          :total="currentDir.length"
        >
        </el-pagination>
      </div>
    </div>
    <!-- 预览模块 -->
    <previewer
      :visible.sync="previewMode"
      ref="previewer"
      @unsupported="previewUnsupported"
      @loading="previewLoading"
    ></previewer>
    <!-- 创建文件夹提示框 -->
    <el-dialog title="创建文件夹" :visible.sync="isMkdir" width="500px">
      <el-form
        :model="mkdirInfo"
        label-width="100px"
        style="margin: auto"
        @submit.native.prevent
        @keyup.enter.native="mkdir"
      >
        <el-form-item label="文件夹名称">
          <el-input v-model="mkdirInfo.name"></el-input>
        </el-form-item>
        <el-form-item label="文件夹简介">
          <el-input type="textarea" v-model="mkdirInfo.description"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="isMkdir = false">取 消</el-button>
        <el-button type="primary" @click="mkdir">确 定</el-button>
      </div>
    </el-dialog>
    <!-- 上传文件提示框 -->
    <el-dialog
      :title="`上传文件到${currentDir.name}`"
      :visible.sync="isUploading"
      width="450px"
    >
      <file-upload
        :upload-method="uploadFunc"
        :on-success="onFileUploadSuccess"
      ></file-upload>
    </el-dialog>
    <!-- 修改文件提示框 -->
    <el-dialog
      :title="`正在修改 ${currentFile ? currentFile.origin.name : ''}`"
      :visible.sync="isChangingFile"
      width="500px"
    >
      <el-form
        v-if="currentFile"
        :model="currentFile"
        label-width="100px"
        style="margin: auto"
        @submit.native.prevent
        @keyup.enter.native="commitChangeFile"
      >
        <el-form-item :label="currentFile.origin.isDir ? '文件夹名' : '文件名'">
          <el-input v-model="currentFile.name"></el-input>
        </el-form-item>
        <el-form-item label="简介">
          <el-input
            type="textarea"
            v-model="currentFile.description"
          ></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer">
        <el-button @click="isChangingFile = false">取 消</el-button>
        <el-button type="primary" @click="commitChangeFile">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import moment from "moment";
import FileUpload from "~/components/file-list/FileUpload";
import Previewer from "~/components/file-list/Previewer";
import * as _ from "lodash";

const CHECKOUT_DIR_EVENT = "checkout-dir";
const CHECKOUT_PAGE_EVENT = "checkout-page";

export default {
  name: "AsyncFileList",
  components: { Previewer, FileUpload },
  props: {
    /**
     * 设定一页的展示的大小
     */
    pageSize: {
      type: Number,
      default: 12,
    },
    /**
     * 加载某文件夹的某页文件 <br>
     *     为以下形式的异步函数 async loadPage(dir,page,pageSize)。
     *     函数将会被自动调用，并传入 dir：文件夹元信息、page：页码（从1开始）、pageSize：单页大小。
     */
    loadPage: {
      type: Function,
      required: true,
    },
    /**
     *  加载根路径元数据 <br>
     *      为以下形式的异步函数 async loadRoot()。
     *      函数将会被自动调用。
     */
    loadRootMeta: {
      type: Function,
      required: true,
    },
    /**
     *  通过id加载指定文件的元信息，应当为符合以下格式的异步函数 <br>
     *      async loadFileMeta(id)
     */
    loadFileMeta: Function,
    /**
     *  修改文件元信息 <br>
     *      类型为以下格式的异步函数 async changeFileMeta(id,targetMeta) <br>
     *          - id: 文件元信息的id
     *          - targetMeta: 目标元信息，具有以下格式:
     *           {
     *               name:"newName",                        // 新的元信息名
     *               description:"new description",         // 新的简介
     *               origin:{                               // 原元信息对象
     *                   id:1,
     *                   name:"oldName",
     *                   description:"old description",
     *                   isDir:false,
     *                   size:1234,
     *                   ...
     *               },
     *               originDir:{                            // 源文件夹
     *                   id:2,
     *                   name:"origin directory",
     *                   isDir:true,
     *                   ...
     *               },
     *               originPage:4                           // 所在页码
     *           }
     */
    changeFileMeta: Function,
    /**
     * 在一个目录下创建一个文件夹 <br>
     *     类型为以下格式的一个异步函数 async mkdir(targetDir,dirName,dirDescription)
     */
    makeDirectory: Function,
    /**
     * 文件上传操作 <br>
     *     类型为一个函数 void function(file)
     */
    uploadMethod: Function,
    uploadSuccess: Function,
    deleteFile: Function,
    downloadFile: Function,
    /**
     * 搜索文件操作 <br>
     *     searchFiles(this.currentDir,page,this.pageSize)
     */
    searchFiles: Function,
    /**
     * 搜索元信息操作 <br>
     *     this.searchMeta(name,dir)
     */
    searchMeta: Function,
    searchPreview: Function,
    previewSource: Function,
    /**
     * 列表名
     */
    labelName: {
      type: String,
      default: "资源",
    },
    /**
     * 是否显示文件大小
     */
    showSize: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示修改日期
     */
    showModifyTime: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示操作列表
     */
    showOperator: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示上传按钮
     */
    showUpload: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示新建文件夹按钮
     */
    showMkdir: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示搜索栏
     */
    showSearch: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示分隔符
     */
    showSeparator: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示类型
     */
    showType: {
      type: Boolean,
      default: true,
    },
    /**
     * 是否显示简介
     */
    showDescription: {
      type: Boolean,
      default: false,
    },
    cacheTime: {
      type: Number,
      default: 60000,
    },
    tableKey: {
      type: Number,
      default: 0,
    },
  },
  data() {
    return {
      tableData: [],
      currentDir: { id: null, uniqueName: null },
      currentPage: 1,
      historyDir: [],
      loading: false,
      lastError: "",
      isMkdir: false,
      isUploading: false,
      mkdirInfo: { name: "", description: "" },
      currentFile: undefined,
      isChangingFile: false,
      searchName: "",
      lastSearchName: "",
      isError: false,
      searchTokenSource: null,
      previewMode: false,
      retryPromise: undefined,
    };
  },
  methods: {
    sizeChange(s) {
      this.$emit("update:page-size", s);
      this.$nextTick(() =>
        this.historyDir.forEach((dir) => this.cleanBuffer(dir))
      );
    },
    /**
     * 在当前目录的基础上前进个目录，并打开此目录。
     */
    openDir(dir) {
      this.historyDir.push(dir);
      this.checkoutDir(dir);
    },
    /**
     * 跳转到某历史文件夹上
     */
    jumpTo(dir) {
      if (dir.id === this.currentDir.id) return undefined;
      let i = this.historyDir.length - 1;
      while (i >= 0 && this.historyDir[i].id !== dir.id) i--;
      this.historyDir = this.historyDir.slice(0, i + 1);
      this.checkoutDir(this.historyDir[i]);
    },
    /**
     * 将当前目录设为某目录，并重置到指定页码上。
     */
    checkoutDir(dir) {
      if (!dir.currentPage) {
        dir.currentPage = 1;
      }
      this.currentDir = dir;
      if (this.currentPage !== dir.currentPage) {
        this.currentPage = dir.currentPage;
      }
      this.$emit(CHECKOUT_DIR_EVENT, dir);
      this.pageChange(this.currentPage);
    },
    /**
     * 格式化毫秒时间戳
     */
    msFormat(ms) {
      return moment(ms).format("YYYY-MM-DD HH:mm:ss");
    },
    /**
     * 当页码被切换时调用。
     */
    async pageChange(page, unableLoading) {
      if (!unableLoading) this.loading = true;
      this.currentDir.currentPage = page;
      const currentId = this.currentDir.id;
      if (!this.currentDir.cachedPage) {
        this.currentDir.cachedPage = [];
        this.currentDir.cachedPage.length = Math.ceil(
          this.currentDir.length / this.pageSize
        );
      }
      if (
        !this.currentDir.cachedPage[page - 1] || //未被缓存过
        this.cacheTime < 0 || //缓存被禁用
        Date.now() - this.currentDir.cachedPage[page - 1].lastCacheTime >=
          this.cacheTime //已被缓存过但已过期
      ) {
        try {
          if (this.currentDir.isSearch === true)
            this.currentDir.cachedPage[page - 1] = await this.searchFiles(
              this.currentDir,
              page,
              this.pageSize
            );
          else
            this.currentDir.cachedPage[page - 1] = await this.loadPage(
              this.currentDir,
              page,
              this.pageSize
            );
          this.currentDir.cachedPage[page - 1].lastCacheTime = Date.now();
          this.currentDir.cachedPage[page - 1].forEach(
            (file) => (file.parent = this.currentDir)
          );
        } catch (e) {
          if (this.currentPage === page && this.currentDir.id === currentId) {
            if (this.currentDir.cachedPage[page - 1]) return undefined;
            this.lastError = e.message;
            this.$notify.error({
              title: "切换失败",
              message: `加载${
                this.labelTitle
              }列表失败:${e.message.toString()}。`,
            });
            this.isError = true;
          }
          return undefined;
        }
      }
      if (this.currentPage === page && this.currentDir.id === currentId) {
        this.tableData = this.currentDir.cachedPage[page - 1];
        this.loading = false;
        this.$emit(CHECKOUT_PAGE_EVENT, page, this.currentDir, this.tableData);
      }
    },
    async loadRoot() {
      this.loading = true;
      try {
        //加载根目录
        this.currentDir = await this.loadRootMeta();
        this.openDir(this.currentDir);
      } catch (e) {
        this.lastError = e.message;
        this.$notify.error({
          title: "初始化失败",
          message: `加载${
            this.labelTitle
          }根列表失败，${e.message.toString()}。`,
        });
        this.isError = true;
        return undefined;
      } finally {
        this.loading = false;
      }
    },
    /**
     * 当上传和删除工作完成后，往往会影响到文件夹元信息（长度信息）。
     * 此函数用于重置当前文件夹的元信息缓存。
     */
    async updateCurrentDirMeta() {
      const dir = this.currentDir;
      if (!this.loadFileMeta || !dir) return undefined;
      try {
        const meta = await this.loadFileMeta(dir.id);
        if (this.currentDir.id !== dir.id) return undefined;
        _.merge(dir, meta);
        dir.cachedPage.length = Math.ceil(dir.length / this.pageSize);
        this.currentDir = dir;
        console.log(this.currentDir);
      } catch (e) {
        // 此函数如果出现错误，将不会做出任何行为。
        console.error(e);
      }
    },
    retry(unableLoading) {
      // 当retry正在执行中，直接返回此promise
      if (this.retryPromise) return this.retryPromise;
      this.isError = false;
      if (this.currentDir.id !== null) {
        // 对于某一级文件夹进行重新加载时
        const updateMetaTask = this.updateCurrentDirMeta();
        const updatePageTask = this.pageChange(this.currentPage, unableLoading);
        const retryPromise = Promise.all([updateMetaTask, updatePageTask]);
        this.retryPromise = retryPromise;
        retryPromise.finally(() => {
          this.retryPromise = undefined;
        });
      } else {
        this.loadRoot();
      }
    },
    cleanBuffer(dir) {
      dir.cachedPage.length = 0;
      dir.cachedPage.length = Math.ceil(dir.length / this.pageSize);
      if (this.currentDir.id === dir.id) this.retry(true);
    },
    async onDelete(file, dir, page) {
      try {
        await this.$confirm(`此操作将删除${file.name}, 是否继续?`, "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        });
      } catch (e) {
        this.$message({
          type: "info",
          message: "已取消删除",
        });
        return undefined;
      }
      try {
        await this.deleteFile(file, dir, page);
        this.$message({
          type: "success",
          message: "删除成功!",
        });
        dir.cachedPage.length = page - 1;
        dir.cachedPage.length = Math.ceil(dir.length / this.pageSize);
        this.retry(true);
      } catch (e) {
        this.$notify.error({
          title: `删除${file.name}失败。`,
          message: e.message,
        });
      }
    },
    onFileChange(file) {
      this.currentFile = {
        name: file.name,
        description: file.description,
        origin: file,
        originDir: this.currentDir,
        originPage: this.currentPage,
      };
      this.isChangingFile = true;
    },
    commitChangeFile() {
      this.isChangingFile = false;
      if (!this.currentFile) return null;
      const origin = this.currentFile.origin;
      const targetName = this.currentFile.name;
      const targetDes = this.currentFile.description;
      if (origin.name !== targetName || origin.description !== targetDes) {
        this.changeFileMeta(origin.id, this.currentFile)
          .then(() => {
            this.$message({
              type: "success",
              message: "修改成功!",
            });
            this.cleanBuffer(this.currentDir);
          })
          .catch((e) => {
            this.$notify.error({
              title: origin.name + " 修改失败!",
              message: e.message,
            });
          });
      }
    },
    async mkdir() {
      this.isMkdir = false;
      if (
        !this.mkdirInfo ||
        !this.mkdirInfo.name ||
        this.mkdirInfo.length === 0
      )
        return null;
      try {
        if (!this.mkdirInfo.description) this.mkdirInfo.description = "";
        await this.makeDirectory(
          this.currentDir,
          this.mkdirInfo.name,
          this.mkdirInfo.description
        );
        this.$message({
          type: "success",
          message: "文件夹创建成功!",
        });
        this.mkdirInfo.name = "";
        this.mkdirInfo.description = "";
        this.cleanBuffer(this.currentDir);
      } catch (e) {
        this.$notify.error({
          title: this.mkdirInfo.name + " 创建失败!",
          message: e.message,
        });
      }
    },
    uploadFunc(file) {
      file.file.currentDir = this.currentDir;
      this.uploadMethod(file, this.currentDir, null);
    },
    onFileUploadSuccess(response, file, fileList) {
      if (file.raw && file.raw.currentDir)
        this.cleanBuffer(file.raw.currentDir);
      if (typeof this.uploadSuccess === "function")
        this.uploadSuccess(response, file, fileList);
    },
    async searchActive(name, dir) {
      if (name === "" || name === this.lastSearchName) return undefined;
      this.loading = true;
      const current = this.currentDir,
        cpg = this.currentPage;
      const targetDir = dir.isSearch ? dir.searchDir : dir;
      try {
        const searchMeta = await this.searchMeta(name, targetDir);
        searchMeta.id = -Math.ceil(Math.random() * 1000000);
        searchMeta.isSearch = true;
        searchMeta.parent = targetDir;
        searchMeta.name = `搜索'${name}'的结果`;
        searchMeta.searchName = name;
        searchMeta.searchDir = targetDir;
        if (this.currentDir.isSearch === true) this.historyDir.pop();
        this.openDir(searchMeta);
      } catch (e) {
        this.$alert(e.message, name + " 搜索失败!", { type: "error" });
        throw e;
      } finally {
        if (current.id === this.currentDir.id && cpg === this.currentPage) {
          this.loading = false;
        }
      }
    },
    async querySearchAsync(queryString, cb) {
      if (!this.searchPreview) cb();
      if (!this.searchTokenSource)
        this.searchTokenSource = this.$http.CancelToken.source();
      else {
        this.searchTokenSource.cancel();
        this.searchTokenSource = this.$http.CancelToken.source();
      }
      const targetDir = this.currentDir.isSearch
        ? this.currentDir.searchDir
        : this.currentDir;
      try {
        const queryAns = await this.searchPreview(
          queryString,
          targetDir,
          this.searchTokenSource.token
        );
        cb(queryAns);
      } catch (e) {
        console.log(e);
      }
    },
    async preview(file) {
      if (!file || file["isDir"] === true) return undefined;
      let src = null;
      try {
        const handle = this.previewSource(file);
        console.log(this.previewSource(file));
        if (typeof handle === "object" && handle.then) {
          src = await handle;
        } else if (typeof handle === "string") {
          src = handle;
        }
      } catch (e) {
        this.$alert(e.message, "预览失败", { type: "error" });
        return undefined;
      }
      if (!src) {
        this.$alert("无法索引到文件资源的位置。", "预览失败", {
          type: "error",
        });
        return undefined;
      }
      this.$refs.previewer.$emit("reload", file.name, file.ext, src, file.id);
    },
    previewUnsupported() {
      this.$alert("无法预览此文件。", "无法预览", { type: "error" });
    },
    previewLoading() {
      this.previewMode = true;
    },
    rowClicked(row, col) {
      if (col.label === "操作") return undefined;
      if (row.isDir === true) {
        this.openDir(row);
      } else if (this.previewSource) {
        this.preview(row);
      }
    },
  },
  async mounted() {
    await this.loadRoot();
  },
  computed: {
    labelTitle() {
      return this.labelName + "名";
    },
    searchTitle() {
      return this.labelName + "名称";
    },
    searchPlaceholder() {
      return "请输入" + this.searchTitle;
    },
  },
  watch: {
    previewMode(v) {
      this.$emit("preview-change", v);
    },
  },
};
</script>

<style scoped>
.search {
  margin-right: 1vw;
}
.breadcrumb {
  cursor: pointer;
}
.breadcrumb:hover {
  color: #409eff;
}
.pagination {
  margin-top: 1px;
  /* margin-left: 65%; */
}
.link {
  color: #014fde;
  cursor: pointer;
}
.link:hover {
  color: #409eff;
  text-decoration: underline;
}
.error-card {
  position: relative;
  cursor: pointer;
  transition: 50ms;
}
.error-card:hover {
  transform: translateY(-5px);
}
.el-table-box {
  display: block;
  height: 68vh;
  overflow-y: auto;
}
.el-table::before {
  z-index: inherit;
}
</style>
